package com.yyaccp.hncc.social.controller;


import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.request.OapiSnsGetuserinfoBycodeRequest;
import com.dingtalk.api.response.OapiSnsGetuserinfoBycodeResponse;
import com.yyaccp.hncc.common.HnccConstants;
import com.yyaccp.hncc.social.domain.SysSocialPlatform;
import com.yyaccp.hncc.social.service.ISysSocialPlatformService;
import com.yyaccp.hncc.social.service.impl.SocialUserLoginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

import static com.yyaccp.hncc.common.SocailContants.*;

/**
 * @author 天天向上
 */
@Slf4j
@Controller
@RequestMapping("/social")
@RequiredArgsConstructor
public class SocialLoginController {

    private final RestTemplate restTemplate;
    private final SocialUserLoginService socialUserLoginService;
    private final ISysSocialPlatformService socialPlatformService;
    @Value("${web.server.url}")
    private String webServerUrl;
    /**
     * 第一步，按对应文档要求请求服务器指定网址，获取code(授权码)
     *
     * @param systemId 平台名字
     * @param response
     * @throws Exception
     */
    @RequestMapping("/authorize/{systemId}")
    public void step1(@PathVariable String systemId, HttpServletResponse response) throws Exception {
        SysSocialPlatform platform = socialPlatformService.selectSysSocialPlatformById(systemId);
        Assert.notNull(platform, "管理员没有配置" + systemId + "登录，请联系管理员");
        if (platform.getStatus() == HnccConstants.STATUS_DISABLED) {
            throw new Exception(systemId + "登录已经被管理员停用");
        }
        StringBuffer bufferUri = new StringBuffer(platform.getServerUrl())
                .append(platform.getAuthorizePath())
                .append("?")
                // 在服务器上申请的app id
                .append("client_id=")
                .append(platform.getClientId())
                // 在服务器上申请的app id,钉钉登录参数名字为appid
                .append("&appid=")
                .append(platform.getClientId())
                // 回调地址，要跟在服务器上设置的回调地址一致
                .append("&redirect_uri=")
                .append(URLEncoder.encode(platform.getCallbackUrl(), "UTF-8"))
                // 告诉服务器，我要请求授权码
                .append("&response_type=code")
                // 钉钉登录需要
                .append("&scope=snsapi_login")
                // qq登录需要
                .append("&state=state");
        if (log.isDebugEnabled()) {
            log.debug("request uri:{}", bufferUri);
        }
        response.sendRedirect(bufferUri.toString());
    }

    /**
     * 第二步
     * 服务器自动回调的控制器
     * 根据服务器返回的授权码获取accessToken
     *
     * @param systemId 平台名字
     * @param code
     * @param model
     */
    @RequestMapping("/{systemId}/callback")
    public String step2(@PathVariable String systemId, String code, String state, Model model) throws Exception {
        SysSocialPlatform platform = socialPlatformService.selectSysSocialPlatformById(systemId);
        Map<String, String> request = new HashMap<>(15);
        // 服务器的网址
        StringBuffer bufferUri = new StringBuffer(platform.getServerUrl())
                // 获取accessToken的路径
                .append(platform.getTokenPath());
        request.put("grant_type", "authorization_code");
        // 授权码
        request.put("code", code);
        // app id
        request.put("client_id", platform.getClientId());
        // 回调地址
        request.put("redirect_uri", platform.getCallbackUrl());
        // app密码
        request.put("client_secret", platform.getClientSecret());
        // qq登录指定返回值类型格式为json
        request.put("fmt", "json");
        if (log.isDebugEnabled()) {
            log.debug("request param {}", request);
        }
        Map<String, Object> returnMap = restTemplate.postForObject(bufferUri.toString(), request, Map.class);
        if (log.isDebugEnabled()) {
            log.debug("callback response result:{}", returnMap);
        }
        // 第三步，通过access_token获取用户信息
        // 如果是微信登录，已经返回了accessToken和openId
        // 如果是QQ登录，还需要使用accessToken获取openId
        // 再使用openId和accessToken和app秘钥获取用户信息
        String accessToken = (String) returnMap.get("access_token");
        bufferUri.setLength(0);
        // 获取用户信息的网址
        bufferUri.append(platform.getUserInfoUrl())
                .append("?access_token=")
                .append(accessToken);
        if (log.isDebugEnabled()) {
            log.debug("request userInfo api:{}", bufferUri);
        }
        Map<String, Object> resultMap = restTemplate.getForObject(bufferUri.toString(), Map.class);
        if (log.isDebugEnabled()) {
            log.debug("request userInfo api result:{}", resultMap);
        }
        if (resultMap == null) {
            throw new Exception("网络不太通畅，无法获取远程" + systemId + "数据");
        } else {
            /**
             * 前后端分离后第三方登录通过html5的window.opener.postMessage来跨域传递消息
             * 具体流程如下：
             * 1、前端点击第三方登录图标
             * 2、前端使用window.open打开新窗口访问后端获取授权码控制器，并添加事件监听
             * 3、第三方应用自动异步回调后端指定控制器（传回授权码）
             * 4、后端回调控制器根据授权码获取accessToken
             * 5、根据accessToken获取第三方应用的用户信息
             * 6、根据第三方用户信息查找关联的hncc系统用户
             * 7、如果没有找到则自动创建关联的hncc系统用户
             * 8、使用hncc系统用户名和密码调用系统登录逻辑(创建token)
             * 9、传递token和前端服务器url到第2步打开的的窗口，通过postMessage传递数据给前端
             * 10、前端事件监听获取postMessage传递的数据（token）,执行前端登录逻辑完成登录
             */
            doSocialLogin(systemId, resultMap, model);
        }
        return "social-login-result.html";
    }

    /**
     * 钉钉自动回调的控制器，会传递code和state
     * 因为后续访问不是标准的oauth2流程（code-->accessToken-->userInfo），所以新写一个方法
     *
     * @param code  临时授权码，只能使用一次，通过code获取用户昵称和openid,unionid
     * @param state
     * @param model
     * @return
     * @throws Exception
     */
    @RequestMapping("/callback/dingtalk")
    public String dingtalkCallback(String code, String state, Model model) throws Exception {
        SysSocialPlatform platform = socialPlatformService.selectSysSocialPlatformById("dingtalk");
        DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/sns/getuserinfo_bycode");
        OapiSnsGetuserinfoBycodeRequest req = new OapiSnsGetuserinfoBycodeRequest();
        req.setTmpAuthCode(code);
        OapiSnsGetuserinfoBycodeResponse response = client.execute(req, platform.getClientId(), platform.getClientSecret());
        if (response.getErrcode().equals(DINGTALK_ERRCODE_SUCCESS)
                && DINGTALK_ERRMSG_SUCCESS.equals(response.getErrmsg())) {
            OapiSnsGetuserinfoBycodeResponse.UserInfo userInfo = response.getUserInfo();
            Map<String, Object> resultMap = new HashMap<>(5);
            resultMap.put("openid", userInfo.getOpenid());
            resultMap.put("unionid", userInfo.getUnionid());
            resultMap.put("name", userInfo.getNick());
            doSocialLogin("dingtalk", resultMap, model);
        } else {
            model.addAttribute(ERROR_KEY, response.getErrmsg());
        }
        return "social-login-result.html";
    }

    private void doSocialLogin(String systemId, Map<String, Object> resultMap, Model model) {
        Assert.notNull(webServerUrl, "请配置前端服务器地址以便进行postMessage传递");
        // 后端生成的token
        String token = socialUserLoginService.socialLogin(resultMap, systemId);
        // 前端需要的token
        model.addAttribute(TOKEN_KEY, token);
        // 前端服务器地址
        model.addAttribute(WEB_SERVER_KEY, webServerUrl);
        if (log.isDebugEnabled()) {
            log.debug("token:{}", token);
        }
    }

    /**
     * 第三方登录局部异常处理
     * 因为要使用window.opener.postMessage传递消息，所以不能使用全局异常处理返回json
     * 必须把数据或错误传递到页面使用js传递
     *
     * @param e
     * @param model
     * @return 视图名字
     */
    @ExceptionHandler(value = {Exception.class})
    public String error(Exception e, Model model) {
        model.addAttribute(ERROR_KEY, e.getMessage());
        // 前端服务器地址
        model.addAttribute(WEB_SERVER_KEY, webServerUrl);
        return "social-login-result.html";
    }

}
